/*
 * Copyright (c) Huawei Technologies Co., Ltd. 2022-2022. All rights reserved.
 */

import { fork } from 'child_process';
import { randomBytes } from 'crypto';
import path from 'path';
import process from 'process';

import { getCurrentProjectCacheDirHash } from '../../../cli/wrapper/util.js';
import { CUR_HVIGOR_VERSION } from '../../../cli/wrapper/wrapper-const.js';
import { HVIGOR_ENGINE_PACKAGE_NAME } from '../../../common/const/const.js';
import { calcChildExecArgv } from '../../../common/util/cacl-exec-argv-util.js';
import { hash } from '@ohos/hvigor-common';
import prettyHrtime from '../../../common/util/pretty-hrtime.js';
import { DAEMON_LISTEN_FAILED_REASON_PREFIX } from '../const/daemon-const.js';
import { HvigorDaemonClientLogger } from '../log/hvigor-daemon-client-log.js';
import { defaultDaemonClientRegistry } from '../registry/daemon-client-registry.js';
import { DaemonInfo, DaemonState } from '../registry/daemon-info.js';
import { SessionIdHelper } from '../session/session-id.js';
import { HvigorConfigReader } from '../../util/hvigor-config-reader.js';

const log = HvigorDaemonClientLogger.getLogger('daemon-client');

/**
 * 生成新daemon process的工厂类
 */
export class DaemonProcessFactory {
  private readonly childExecArgv: string[];

  // 在启动daemon时，会给这个变量赋值，在需要等daemon启动完毕才能执行的场景下，可以await createDaemonPromise来等待daemon
  public createDaemonPromise: Promise<void> | undefined = undefined;

  constructor() {
    this.childExecArgv = calcChildExecArgv();
  }

  async getProjectCompatibleIdleDaemon(needAwait = true): Promise<DaemonInfo | undefined> {
    const projectDaemonInfo: DaemonInfo | undefined = defaultDaemonClientRegistry.findProjectDaemon();

    // 从注册表中没有找到任何一个跟该工程相关的daemon process
    if (projectDaemonInfo === undefined) {
      log.debug(`hvigor client: Cannot find a valid daemon for project, projectPath=${process.cwd()}`);

      // 如果当前注册表或进程中已经存在6个以上的可用的daemon process
      if (defaultDaemonClientRegistry.isAvailableDaemonOverLimit()) {
        const lruDaemonInfo = defaultDaemonClientRegistry.findIdleDaemonWithLRU();

        // 没有找到一个最长未使用的Idle状态的Daemon,则需要直接退出成冷启动的方式
        if (lruDaemonInfo === undefined) {
          log.debug('hvigor client: Cannot find another idle daemon. This build will be executed with no-daemon mode.');
          return undefined;
        } else {
          // 找到则需要将该daemon process关闭,再创建一个新的
          const pid = lruDaemonInfo.pid;
          log.debug(`hvigor client: Idle daemon (pid=${pid}) will be stopped, and a new daemon will be created.`);
          return this.killOldProcessAndCreateNew(lruDaemonInfo, 'daemon process exceeds the upper limit.');
        }
      } else {
        // 没有超过6个则可以直接创建一个新的daemon process
        return this.createNewIdleDaemon(needAwait);
      }
    }

    function checkHvigorVersionChanged(projectDaemonInfo: DaemonInfo) {
      return projectDaemonInfo.hvigorVersion !== CUR_HVIGOR_VERSION;
    }

    function checkNodeEnvChanged(projectDaemonInfo: DaemonInfo, childExecArgv: string[]) {
      log.debug(`check node env with process execArgv, execArgv=${childExecArgv}`);
      const currentPkgPath = require.resolve(HVIGOR_ENGINE_PACKAGE_NAME);
      return (
        projectDaemonInfo.nodeVersion !== process.version ||
        projectDaemonInfo.nodeParams !== hash(`${currentPkgPath}${getCurrentProjectCacheDirHash()}${childExecArgv.toString()}`)
      );
    }

    const isHvigorVersionChanged = checkHvigorVersionChanged(projectDaemonInfo);
    const isNodeEnvChanged = checkNodeEnvChanged(projectDaemonInfo, this.childExecArgv);

    if (isHvigorVersionChanged || isNodeEnvChanged) {
      log.info(
        'hvigor client: The argument passed to Node.js or hvigor version have changed ' +
          'and a new daemon will be created to replace the existing daemon. Reason:' +
          `{isHvigorVersionChanged: ${isHvigorVersionChanged}, isNodeEnvChanged: ${isNodeEnvChanged}}`,
      );
      return this.killOldProcessAndCreateNew(projectDaemonInfo!, 'stopped by hvigor, runtime has changed.');
    }
    return projectDaemonInfo;
  }

  private async killOldProcessAndCreateNew(lruDaemonInfo: DaemonInfo, stopProcessReason: string): Promise<DaemonInfo | undefined> {
    // 直接通过client侧更新注册表的信息,设置该daemon为stopped状态,并通过发送信号让process自动关闭
    defaultDaemonClientRegistry.updateDaemonProcessState(lruDaemonInfo, DaemonState.STOPPED, stopProcessReason);
    return this.createNewIdleDaemon();
  }

  private async createNewIdleDaemon(needAwait = true): Promise<DaemonInfo | undefined> {
    log.info('hvigor client: Starting hvigor daemon.');
    const startDaemonBeginTime = process.hrtime();

    let newDaemonInfo;
    const sessionId = randomBytes(32).toString('hex');
    const encryptSessionId = SessionIdHelper.encryptPwdByDefaultMaterial(sessionId, log);
    // process.argv中设置的node参数优先级高于hvigor-config，需要传入子进程中
    const nodeParamsFromProcessArgv = HvigorConfigReader.getNodeParamFromProcessArgv();

    const serverProcess = fork(
      path.resolve(__dirname, 'daemon-process-boot-script.js'),
      [log.getLevel().toString(), encryptSessionId, ...nodeParamsFromProcessArgv],
      {
        stdio: ['pipe', 'pipe', 'pipe', 'ipc'],
        detached: true,
        env: {
          ...process.env,
          _logLevel: log.getLevel().toString(),
          _sessionId: encryptSessionId,
        },
        execArgv: this.childExecArgv,
      },
    );

    let waitConnectTimeout;
    this.createDaemonPromise = new Promise<void>((resolve) => {
      serverProcess.once('message', (data: string) => {
        if (data.startsWith(DAEMON_LISTEN_FAILED_REASON_PREFIX)) {
          log.warn('hvigor client: daemon failed to listen on the port. Build will be started in no-daemon mode. ' + `Reason: ${data}`);
          newDaemonInfo = undefined;
        } else {
          newDaemonInfo = JSON.parse(data);
          log.info(`Hvigor Daemon started in ${prettyHrtime(process.hrtime(startDaemonBeginTime))}`);

          log.debug(`A new daemon has been created successfully and is listening for requests, daemonInfo=${data}`);
        }
        resolve();
      });

      // 阻塞主进程, 等待子进程的创建server并建立监听后的结果才能继续往后执行
      waitConnectTimeout = setTimeout(() => {
        serverProcess.off('message', () => {
          log.warn('hvigor client: Failed to start hvigor daemon. The build will be started in no-daemon mode.');
          if (serverProcess.pid) {
            log.warn('hvigor client: This should be an error while initializing hvigor daemon.');
            log.daemonWarnLink(serverProcess.pid);
          } else {
            log.warn("hvigor client: This should be an error that hvigor daemon didn't start.");
          }
        });
        resolve();
      }, 10000);
    });
    if (needAwait) {
      await this.createDaemonPromise;
    }

    clearTimeout(waitConnectTimeout);
    serverProcess.unref();

    return newDaemonInfo;
  }

  /**
   * 新启动一个node进程去创建daemon
   */
  public createDaemonFork() {
    this.getProjectCompatibleIdleDaemon(false);
  }
}

export const defaultDaemonServerFactory = new DaemonProcessFactory();
